package net.lgy.component;


import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.Bucket;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.S3ObjectSummary;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;


/**
 * MinIO 文件存储引擎实现类（策略模式的 ConcreteStrategy 角色）
 * 基于 Amazon S3 协议兼容层，实现 StoreEngine 抽象接口定义的所有存储能力
 * 核心适配 MinIO 服务，支持桶管理、文件上传/下载/删除、临时URL生成等功能
 */
@Component // 注册为Spring Bean，供上下文（Context）自动发现和使用
@Slf4j    // Lombok注解，提供日志输出能力，避免手动创建Logger对象
public class MinioFileStoreEngine implements StoreEngine{

    /**
     * 注入 Amazon S3 客户端实例
     * 该实例由配置类（如 AmazonS3Config）创建，已完成 MinIO 服务的连接配置（Endpoint、密钥等）
     * 因 MinIO 兼容 S3 协议，可直接使用 AmazonS3Client 操作 MinIO
     */
    @Resource
    private AmazonS3Client amazonS3Client;


    // ======================== 存储桶（Bucket）管理方法 ========================

    /**
     * 判断指定存储桶是否存在
     * @param bucketName 存储桶名称（需符合 MinIO/S3 命名规范：小写字母、数字、连字符，无特殊符号）
     * @return true=桶存在，false=桶不存在（或参数非法）
     */
    @Override
    public boolean bucketExists(String bucketName) {
        // 前置校验：桶名不可为空，避免无效调用
        if (isEmpty(bucketName)) {
            log.warn("桶名不可为空，无法判断是否存在");
            return false;
        }
        try {
            // 调用 S3 客户端API判断桶存在性（MinIO 兼容该接口）
            return amazonS3Client.doesBucketExistV2(bucketName);
        } catch (Exception e) {
            // 捕获异常并记录日志（如网络异常、权限不足等），避免向上抛出非业务异常
            log.error("判断桶[{}]是否存在失败，异常信息：{}", bucketName, e.getMessage(), e);
            return false;
        }
    }

    /**
     * 删除指定存储桶
     * 注意：MinIO/S3 要求删除前桶必须为空（无任何文件/版本），否则会抛异常
     * @param bucketName 待删除的存储桶名称
     * @return true=删除成功，false=删除失败（桶不存在/非空/权限不足）
     */
    @Override
    public boolean removeBucket(String bucketName) {
        // 先判断桶是否存在，避免无效删除
        if (!bucketExists(bucketName)) {
            log.warn("删除失败：桶[{}]不存在", bucketName);
            return false;
        }
        try {
            // 调用 API 删除桶（若桶非空，会抛出 AmazonServiceException）
            amazonS3Client.deleteBucket(bucketName);
            log.info("桶[{}]删除成功", bucketName);
            return true;
        } catch (Exception e) {
            log.error("删除桶[{}]失败，异常信息：{}", bucketName, e.getMessage(), e);
            return false;
        }
    }

    /**
     * 创建存储桶（若桶不存在）
     * @param bucketName 待创建的存储桶名称（需全局唯一，跨 MinIO 实例）
     */
    @Override
    public void createBucket(String bucketName) {
        // 前置校验：桶名不可为空
        if (isEmpty(bucketName)) {
            log.error("创建桶失败：桶名不可为空");
            throw new IllegalArgumentException("桶名不可为空");
        }
        // 先判断桶是否已存在，避免重复创建
        if (bucketExists(bucketName)) {
            log.warn("桶[{}]已存在，无需重复创建", bucketName);
            return;
        }
        try {
            // 调用 API 创建桶（MinIO 会自动在配置的存储路径下生成桶目录）
            amazonS3Client.createBucket(bucketName);
            log.info("桶[{}]创建成功", bucketName);
        } catch (Exception e) {
            log.error("创建桶[{}]失败，异常信息：{}", bucketName, e.getMessage(), e);
            throw new RuntimeException("创建桶失败：" + e.getMessage(), e);
        }
    }

    /**
     * 获取当前 MinIO 实例下的所有存储桶
     * @return 桶列表（Bucket 对象包含桶名、创建时间等信息），无桶时返回空列表
     */
    @Override
    public List<Bucket> getAllBucket() {
        try {
            List<Bucket> buckets = amazonS3Client.listBuckets();
            log.info("获取所有桶成功，共[{}]个桶", buckets.size());
            return buckets;
        } catch (Exception e) {
            log.error("获取所有桶失败，异常信息：{}", e.getMessage(), e);
            return List.of(); // 返回空列表，避免返回null导致NPE
        }
    }


    // ======================== 文件（Object）管理方法 ========================

    /**
     * 列举指定桶下的所有文件（Object）
     * @param bucketName 存储桶名称
     * @return 文件摘要列表（S3ObjectSummary 包含文件名、大小、最后修改时间等），桶不存在时返回空列表
     */
    @Override
    public List<S3ObjectSummary> listObjects(String bucketName) {
        // 先判断桶是否存在
        if (!bucketExists(bucketName)) {
            log.warn("列举文件失败：桶[{}]不存在", bucketName);
            return List.of();
        }
        try {
            // 调用 API 列举桶内文件（默认分页，此处获取第一页；若文件过多需处理分页）
            List<S3ObjectSummary> summaries = amazonS3Client.listObjects(bucketName).getObjectSummaries();
            log.info("桶[{}]下共列举到[{}]个文件", bucketName, summaries.size());
            return summaries;
        } catch (Exception e) {
            log.error("列举桶[{}]下文件失败，异常信息：{}", bucketName, e.getMessage(), e);
            return List.of();
        }
    }

    /**
     * 判断指定文件是否在桶中存在
     * @param bucketName 存储桶名称
     * @param objectKey 文件在桶中的唯一标识（类似路径+文件名，如 "images/avatar.jpg"）
     * @return true=文件存在，false=文件不存在/桶不存在/参数非法
     */
    @Override
    public boolean doesObjectExist(String bucketName, String objectKey) {
        // 前置校验：桶名和文件标识不可为空
        if (isEmpty(bucketName) || isEmpty(objectKey)) {
            log.warn("判断文件存在失败：桶名或文件标识不可为空");
            return false;
        }
        // 先判断桶是否存在
        if (!bucketExists(bucketName)) {
            log.warn("判断文件存在失败：桶[{}]不存在", bucketName);
            return false;
        }
        try {
            // 调用 API 判断文件存在性（比 listObjects 更高效，直接定位文件）
            return amazonS3Client.doesObjectExist(bucketName, objectKey);
        } catch (Exception e) {
            log.error("判断桶[{}]中文件[{}]是否存在失败，异常信息：{}", bucketName, objectKey, e.getMessage(), e);
            return false;
        }
    }


    // ======================== 文件上传方法 ========================

    /**
     * 从本地文件路径上传文件到指定桶
     * @param bucketName 存储桶名称
     * @param objectKey 目标文件标识（桶内唯一路径）
     * @param localFileName 本地文件绝对路径（如 "D:/temp/test.jpg"）
     * @return true=上传成功，false=上传失败（桶不存在/文件不存在/权限不足）
     */
    @Override
    public boolean upload(String bucketName, String objectKey, String localFileName) {
        // 前置校验：参数不可为空
        if (isEmpty(bucketName) || isEmpty(objectKey) || isEmpty(localFileName)) {
            log.warn("本地文件上传失败：桶名、文件标识或本地路径不可为空");
            return false;
        }
        // 校验本地文件是否存在
        File localFile = new File(localFileName);
        if (!localFile.exists() || !localFile.isFile()) {
            log.warn("本地文件上传失败：文件[{}]不存在或不是有效文件", localFileName);
            return false;
        }
        // 校验桶是否存在
        if (!bucketExists(bucketName)) {
            log.warn("本地文件上传失败：桶[{}]不存在", bucketName);
            return false;
        }
        try {
            // 调用 API 上传本地文件（SDK 内部会处理文件流读取）
            amazonS3Client.putObject(bucketName, objectKey, localFile);
            log.info("本地文件[{}]上传到桶[{}]成功，目标标识：{}", localFileName, bucketName, objectKey);
            return true;
        } catch (Exception e) {
            log.error("本地文件[{}]上传到桶[{}]失败，异常信息：{}", localFileName, bucketName, e.getMessage(), e);
            return false;
        }
    }

    /**
     * 从 MultipartFile 上传文件（适配 Spring MVC 前端文件上传）
     * @param bucketName 存储桶名称
     * @param objectKey 目标文件标识（桶内唯一路径）
     * @param file 前端上传的 MultipartFile 对象（包含文件流、文件名、类型等信息）
     * @return true=上传成功，false=上传失败（桶不存在/文件为空/流读取异常）
     */
    @Override
    public boolean upload(String bucketName, String objectKey, MultipartFile file) {
        // 前置校验：参数不可为空，文件不可为空
        if (isEmpty(bucketName) || isEmpty(objectKey) || Objects.isNull(file) || file.isEmpty()) {
            log.warn("MultipartFile 上传失败：桶名、文件标识不可为空，且文件不可为空");
            return false;
        }
        // 校验桶是否存在
        if (!bucketExists(bucketName)) {
            log.warn("MultipartFile 上传失败：桶[{}]不存在", bucketName);
            return false;
        }
        // 处理文件流（try-with-resources 自动关闭流，避免资源泄露）
        try (InputStream inputStream = file.getInputStream()) {
            // 构建文件元数据：用于描述文件类型、大小等，帮助 MinIO 正确识别文件
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentType(file.getContentType()); // 设置 MIME 类型（如 image/jpeg、application/pdf）
            metadata.setContentLength(file.getSize());     // 设置文件大小（字节），避免 SDK 自动计算

            // 调用 API 上传文件流
            amazonS3Client.putObject(bucketName, objectKey, inputStream, metadata);
            log.info("MultipartFile [{}] 上传到桶[{}]成功，目标标识：{}，文件大小：{}KB",
                    file.getOriginalFilename(), bucketName, objectKey, file.getSize() / 1024);
            return true;
        } catch (IOException e) {
            log.error("MultipartFile 流读取失败，原文件名：{}，异常信息：{}", file.getOriginalFilename(), e.getMessage(), e);
            return false;
        } catch (Exception e) {
            log.error("MultipartFile [{}] 上传到桶[{}]失败，异常信息：{}",
                    file.getOriginalFilename(), bucketName, e.getMessage(), e);
            return false;
        }
    }


    // ======================== 文件删除方法 ========================

    /**
     * 删除指定桶中的文件
     * @param bucketName 存储桶名称
     * @param objectKey 待删除文件的标识
     * @return true=删除成功，false=删除失败（桶不存在/文件不存在/权限不足）
     */
    @Override
    public boolean delete(String bucketName, String objectKey) {
        // 前置校验：参数不可为空
        if (isEmpty(bucketName) || isEmpty(objectKey)) {
            log.warn("文件删除失败：桶名或文件标识不可为空");
            return false;
        }
        // 先判断桶和文件是否存在，避免无效删除
        if (!bucketExists(bucketName)) {
            log.warn("文件删除失败：桶[{}]不存在", bucketName);
            return false;
        }
        if (!doesObjectExist(bucketName, objectKey)) {
            log.warn("文件删除失败：桶[{}]中文件[{}]不存在", bucketName, objectKey);
            return false;
        }
        try {
            // 调用 API 删除文件
            amazonS3Client.deleteObject(bucketName, objectKey);
            log.info("桶[{}]中文件[{}]删除成功", bucketName, objectKey);
            return true;
        } catch (Exception e) {
            log.error("删除桶[{}]中文件[{}]失败，异常信息：{}", bucketName, objectKey, e.getMessage(), e);
            return false;
        }
    }


    // ======================== 文件下载与临时URL方法 ========================

    /**
     * 生成文件的临时下载URL（预签名URL）
     * 特点：URL包含临时访问权限，过期后失效，无需暴露MinIO密钥
     * @param bucketName 存储桶名称
     * @param remoteFileName 文件标识（与上传时的 objectKey 一致）
     * @param timeout 有效期时长
     * @param unit 有效期单位（如 TimeUnit.DAYS、TimeUnit.HOURS）
     * @return 临时下载URL（成功）/ null（失败）
     */
    @Override
    public String getDownloadUrl(String bucketName, String remoteFileName, long timeout, TimeUnit unit) {
        // 前置校验：参数不可为空，有效期需大于0
        if (isEmpty(bucketName) || isEmpty(remoteFileName) || Objects.isNull(unit) || timeout <= 0) {
            log.warn("生成临时URL失败：参数非法（桶名/文件名不可为空，有效期需大于0）");
            return null;
        }
        // 校验桶和文件是否存在
        if (!bucketExists(bucketName) || !doesObjectExist(bucketName, remoteFileName)) {
            log.warn("生成临时URL失败：桶[{}]或文件[{}]不存在", bucketName, remoteFileName);
            return null;
        }
        try {
            // 计算URL过期时间：当前时间 + 有效期
            Date expiration = new Date(System.currentTimeMillis() + unit.toMillis(timeout));
            // 调用 API 生成预签名URL（默认GET请求，用于下载）
            return amazonS3Client.generatePresignedUrl(bucketName, remoteFileName, expiration).toString();
        } catch (Exception e) {
            log.error("生成桶[{}]中文件[{}]的临时URL失败，异常信息：{}", bucketName, remoteFileName, e.getMessage(), e);
            return null;
        }
    }

    /**
     * 将文件直接下载到 HTTP 响应（适配 Web 场景，前端可直接触发下载）
     * @param bucketName 存储桶名称
     * @param objectKey 待下载文件的标识
     * @param response HTTP响应对象（用于设置下载头、写入文件流）
     */
    @Override
    @SneakyThrows // Lombok注解，自动捕获并包装受检异常为RuntimeException，简化代码
    public void download2Response(String bucketName, String objectKey, HttpServletResponse response) {
        // 前置校验：参数不可为空
        if (isEmpty(bucketName) || isEmpty(objectKey) || Objects.isNull(response)) {
            log.error("文件下载到响应失败：桶名、文件标识或响应对象不可为空");
            throw new IllegalArgumentException("参数非法：桶名、文件标识或响应对象不可为空");
        }
        // 校验桶和文件是否存在
        if (!bucketExists(bucketName) || !doesObjectExist(bucketName, objectKey)) {
            log.error("文件下载到响应失败：桶[{}]或文件[{}]不存在", bucketName, objectKey);
            throw new RuntimeException("文件不存在：桶[" + bucketName + "], 文件[" + objectKey + "]");
        }

        // 从 MinIO 获取文件对象（包含文件流）
        try (S3Object s3Object = amazonS3Client.getObject(bucketName, objectKey);
             InputStream inputStream = s3Object.getObjectContent();
             OutputStream outputStream = response.getOutputStream()) {

            // 设置 HTTP 响应头：告诉浏览器以"附件"形式下载文件
            String fileName = objectKey.substring(objectKey.lastIndexOf("/") + 1); // 提取文件名（去掉路径）
            response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
            // 设置响应类型：强制下载（不预览）
            response.setContentType("application/force-download");
            // 设置字符编码：避免文件名中文乱码
            response.setCharacterEncoding("UTF-8");

            // 将 MinIO 文件流写入响应流（IOUtils 简化流拷贝）
            IOUtils.copy(inputStream, outputStream);
            // 强制刷新响应流，确保数据全部发送到前端
            outputStream.flush();
            log.info("文件[{}]从桶[{}]下载到响应成功，文件名：{}", objectKey, bucketName, fileName);

        } catch (IOException e) {
            log.error("文件流拷贝失败，桶[{}]，文件[{}]，异常信息：{}", bucketName, objectKey, e.getMessage(), e);
            throw new RuntimeException("文件下载失败：流拷贝错误", e);
        } catch (Exception e) {
            log.error("文件[{}]从桶[{}]下载到响应失败，异常信息：{}", bucketName, objectKey, e.getMessage(), e);
            throw new RuntimeException("文件下载失败：" + e.getMessage(), e);
        }
    }


    // ======================== 工具方法 ========================

    /**
     * 字符串空值判断（工具方法，避免重复代码）
     * @param str 待判断字符串
     * @return true=为空（null或空白字符），false=非空
     */
    private boolean isEmpty(String str) {
        return str == null || str.trim().isEmpty();
    }
}
